﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UlsStudio.Engine.Events;
using UlsStudio.Engine.Parsers;

namespace UlsStudio.Engine
{
    /// <summary>
    /// Keeps an eye on ULS log updates - new & existing
    /// </summary>
    public class LogFileWatcher
    {
        #region Privates and Constructors

        const int FAIL_COUNT_MAX = 3;

        Dictionary<string, LogfileInfo> _lastCacheUpdate = null;
        private Dictionary<string, int> _problemFileCount = null;
        private readonly List<DirectoryInfo> _directoriesToWatch = null;
        private bool _continueWatching = true;
        private bool _isWatchingFiles = false;

        public LogFileWatcher(ScanSettings settings)
        {
            this.ProblemFileCount = new Dictionary<string, int>();
            this._directoriesToWatch = new List<DirectoryInfo>();
            this.Settings = settings;
        }
        public LogFileWatcher(DirectoryInfo directoryToWatch, ScanSettings settings)
            : this(new List<DirectoryInfo>(new DirectoryInfo[] { directoryToWatch }), settings) { }
        public LogFileWatcher(List<DirectoryInfo> directoriesToWatch, ScanSettings settings)
            : this(settings)
        {
            this._directoriesToWatch = directoriesToWatch;
            this.Settings = settings;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Directories to monitor
        /// </summary>
        public List<DirectoryInfo> DirectoriesToWatch
        {
            get { return _directoriesToWatch; }
        }

        /// <summary>
        /// Keep watching file-changes
        /// </summary>
        public bool ContinueWatching
        {
            get { lock (continueWatchingLock) { return _continueWatching; } }
            set { lock (continueWatchingLock) { _continueWatching = value; } }
        }
        static object continueWatchingLock = new object();


        /// <summary>
        /// Watching going on
        /// </summary>
        public bool IsWatchingFiles
        {
            get { lock (continueWatchingLock) { return _isWatchingFiles; } }
            set { lock (continueWatchingLock) { _isWatchingFiles = value; } }
        }

        /// <summary>
        /// Problematic files - used to know which to avoid
        /// </summary>
        public Dictionary<string, int> ProblemFileCount
        {
            get { return _problemFileCount; }
            set { _problemFileCount = value; }
        }


        /// <summary>
        /// Last logfile information
        /// </summary>
        private Dictionary<string, LogfileInfo> KnownFiles
        {
            get { return _lastCacheUpdate; }
            set { _lastCacheUpdate = value; }
        }

        public ScanSettings Settings { get; set; }

        #endregion

        #region Monitoring Methods

        /// <summary>
        /// Starts monitoring the 
        /// </summary>
        public void BeginMonitoring(DateTime lastRecord)
        {
            // Sanity 
            if (this.DirectoriesToWatch == null || this.DirectoriesToWatch.Count == 0)
            {
                throw new ArgumentNullException("Nothing to watch");
            }

            // Check each dir exists
            string dirNames = string.Empty; const string SEP = ", ";
            foreach (DirectoryInfo dir in this.DirectoriesToWatch)
            {
                if (!dir.Exists)
                {
                    throw new ArgumentOutOfRangeException("directoriesToWatch", string.Format("{0} does not exist", dir.FullName));
                }
                dirNames += string.Format("{0}{1}", dir.FullName, SEP);
            }
            dirNames = dirNames.TrimEnd(SEP.ToCharArray());

            // Debug
            Logging.Writeline("LogFileWatcher - looking for events happening after {0} in {1}", lastRecord, dirNames);


            // Vars
            this.ContinueWatching = true;
            this.IsWatchingFiles = true;

            // Start thread
            System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(this.MonitorForLogChanges));
            t.Name = "Log file-system watcher";
            t.Start(lastRecord);
        }

        /// <summary>
        /// Monitor for new & updated logs
        /// </summary>
        private void MonitorForLogChanges(DateTime lastLogEntryTimeStamp)
        {
            // Build cache
            KnownFiles = GetDirLogfileInfo();

            // Raise events
            OnWatcherStarted(this.DirectoriesToWatch);

            DateTime lastCheck = DateTime.MinValue;
            while (this.ContinueWatching)
            {
                // Keep checking if we need to update every 0.5 seconds but don't update until at least 3 seconds 
                int secondsSinceLastUpdate = DateTime.Now.Subtract(lastCheck).Seconds;
                if (secondsSinceLastUpdate < 3)
                {
                    System.Threading.Thread.Sleep(500);
                }
                else
                {
                    lastCheck = DateTime.Now;
                    RunChangeCheck(lastLogEntryTimeStamp);
                }
            }       // While

            this.IsWatchingFiles = false;
            OnWatcherStopped();     // Raise event

            Logging.Writeline("LogFileWatcher - closed down.");
        }
        private void MonitorForLogChanges(object lastUpdateObj) { MonitorForLogChanges((DateTime)lastUpdateObj); }

        private void RunChangeCheck(DateTime lastLogEntryTimeStamp)
        {
            // Check on files now
            Dictionary<string, FileInfo> currentFiles = GetDirFileInfo();

            // Check each file now found
            foreach (string currentFileName in currentFiles.Keys)
            {
                FileInfo currentFile = currentFiles[currentFileName];

                // Does the old cache contain this file?
                bool fileIsNew = !KnownFiles.ContainsKey(currentFileName);
                if (fileIsNew)
                {
                    List<string> fileData = new List<string>();

                    // Add to known log store (if file is valid)
                    if (AddNewFileToLogCache(currentFile, fileData))
                    {
                        // Add all items
                        Logging.Writeline("Adding {0} lines from new file {1}", fileData.Count, currentFile.Name);
                        DateTime thisUpdate = ProcessNewLines(fileData, lastLogEntryTimeStamp);
                        if (thisUpdate > lastLogEntryTimeStamp)
                            lastLogEntryTimeStamp = thisUpdate;
                    }
                }
                else
                {

                    // File existed before
                    LogfileInfo oldFile = KnownFiles[currentFileName];

                    // Has it been updated?
                    if (currentFile.LastWriteTime != oldFile.File.LastWriteTime)
                    {
                        DateTime thisUpdate = UpdateCacheForFile(currentFile, oldFile, lastLogEntryTimeStamp);
                        if (thisUpdate > lastLogEntryTimeStamp)
                            lastLogEntryTimeStamp = thisUpdate;
                    }
                }
            }
        }
        #endregion

        #region File Update Methods

        /// <summary>
        /// Update the cache for exising file with new caching info
        /// </summary>
        /// <returns>Last updated date</returns>
        private DateTime UpdateCacheForFile(FileInfo newFileInfo, LogfileInfo oldFile, DateTime lastLogEntryTimeStamp)
        {
            string oldLastLine = oldFile.LastReadLine;
            Logging.Writeline("LogFileWatcher - File {0} has changed", newFileInfo.Name);

            // Get file contents that's been updated (while also getting new last line)
            oldFile.File = newFileInfo;
            List<string> updatedFileData = new List<string>();
            string newLastLine = GetLastFileContents(newFileInfo, updatedFileData);
            DateTime lastRecordToBeUpdated = lastLogEntryTimeStamp;

            // Find in old file where old line was, if there was a last line
            int oldExistingLineIdx = 0;
            if (!string.IsNullOrEmpty(oldLastLine))
            {

                foreach (string existingFileLine in updatedFileData)
                {
                    if (oldLastLine == existingFileLine)
                        break;
                    oldExistingLineIdx++;
                }

                // Truncate old file data
                if (oldExistingLineIdx > 0)
                {
                    // Chop lines after last old line
                    updatedFileData.RemoveRange(0, oldExistingLineIdx);
                }
                else
                {
                    Logging.Writeline("Old line not found, adding all file data");
                }
            }

            // Process new lines - return last record to be updated
            lastRecordToBeUpdated = ProcessNewLines(updatedFileData, lastLogEntryTimeStamp);

            // Update cache record
            oldFile.LastReadLine = newLastLine;

            return lastRecordToBeUpdated;
        }

        /// <summary>
        /// Add a new logfile to the known logs store
        /// </summary>
        private bool AddNewFileToLogCache(FileInfo currentFile, List<string> fileData)
        {
            // File is new - get data
            bool addFile = false;

            // Don't process file if it's failed too many times
            bool checkFile = (!ProblemFileCount.ContainsKey(currentFile.FullName) ||
                (ProblemFileCount.ContainsKey(currentFile.FullName) && ProblemFileCount[currentFile.FullName] < FAIL_COUNT_MAX));
            if (checkFile)
            {
                // Add to cache
                LogfileInfo newLogInf = null;
                try
                {
                    // Get file-contents
                    newLogInf = new LogfileInfo(currentFile, GetLastFileContents(currentFile, fileData));
                    addFile = true;
                    Logging.Writeline("LogFileWatcher - File {0} is new", currentFile.Name);
                }
                catch (IOException ex)
                {
                    // Stop a locked file from being read every tim
                    if (ProblemFileCount.ContainsKey(currentFile.FullName))
                    {
                        // Get fail count
                        int failCount = ProblemFileCount[currentFile.FullName] + 1;
                        ProblemFileCount[currentFile.FullName] = failCount;

                        if (failCount < FAIL_COUNT_MAX)
                        {
                            Logging.Writeline("ERROR: Cannot line read logfile {0} - {1}", currentFile.Name, ex.Message);
                        }
                        else if (failCount == FAIL_COUNT_MAX)
                        {
                            Logging.Writeline("ERROR: Now ignoring logfile {0} - {1}", currentFile.Name, ex.Message);
                        }
                    }
                    else
                    {
                        ProblemFileCount.Add(currentFile.FullName, 1);
                    }
                }

                if (addFile)
                {
                    KnownFiles.Add(currentFile.FullName, newLogInf);
                    return true;
                }
                else
                {
                    return false;
                }
            }       // End file-fail count
            else
            {
                return false;
            }
        }

        #endregion

        #region Event Definitions

        public delegate void NewLogEntriesDelegate(object sender, NewEventLogsEventArgs e);

        /// <summary>
        /// Triggered when new log-entries are detected
        /// </summary>
        public event NewLogEntriesDelegate NewLogEntries;

        private void OnNewLogEntries(LogDataCollection newLogEntries)
        {
            if (NewLogEntries != null)
            {
                RealTimeLoadingStatus status = new RealTimeLoadingStatus(this.Settings, newLogEntries.Count);
                NewLogEntries(this, new NewEventLogsEventArgs(newLogEntries, this.Settings));
            }
        }

        /// <summary>
        /// Triggers when the watcher has started
        /// </summary>
        public event EventHandler<WatcherStartedEventArgs> WatcherStarted;
        private void OnWatcherStarted(List<DirectoryInfo> directoryTargets)
        {
            if (WatcherStarted != null)
            {
                WatcherStarted(this, new WatcherStartedEventArgs(directoryTargets));
            }
        }
        

        /// <summary>
        /// Triggers when the watcher stops
        /// </summary>
        public event EventHandler WatcherStopped;
        private void OnWatcherStopped()
        {
            if (WatcherStopped != null)
            {
                WatcherStopped(this, EventArgs.Empty);
            }
        }

        #endregion

        #region Enumerate Folder Methods
        /// <summary>
        /// Get all logs files in all directories with last line of content
        /// </summary>
        /// <returns></returns>
        private Dictionary<string, LogfileInfo> GetDirLogfileInfo()
        {
            Dictionary<string, LogfileInfo> lastCacheUpdate = new Dictionary<string, LogfileInfo>();
            foreach (DirectoryInfo dir in this.DirectoriesToWatch)
            {
                foreach (FileInfo fileInDirectory in dir.GetFiles(Constants.ULS_LOG_SEARCH_PATTERN))
                {
                    string lastFileLine = string.Empty;
                    bool addFile = false;
                    try
                    {
                        lastFileLine = string.Empty; //GetLastFileContents(fileInDirectory);
                        addFile = true;
                    }
                    catch (IOException ex)
                    {
                        Logging.Writeline("ERROR: Cannot line read logfile {0} - {1}", fileInDirectory.Name, ex.Message);
                    }

                    if (addFile)
                        lastCacheUpdate.Add(fileInDirectory.FullName, new LogfileInfo(fileInDirectory, lastFileLine));
                }

            }
            return lastCacheUpdate;
        }

        /// <summary>
        /// Get all logs files in all directories only
        /// </summary>
        /// <returns></returns>
        Dictionary<string, FileInfo> GetDirFileInfo()
        {
            Dictionary<string, FileInfo> filesInDirs = new Dictionary<string, FileInfo>();
            foreach (DirectoryInfo dir in this.DirectoriesToWatch)
            {
                foreach (FileInfo fileInDirectory in dir.GetFiles(Constants.ULS_LOG_SEARCH_PATTERN))
                {
                    filesInDirs.Add(fileInDirectory.FullName, fileInDirectory);
                }
            }
            return filesInDirs;
        }

        #endregion

        /// <summary>
        /// Converts new lines into new ULS objects
        /// </summary>
        /// <param name="fileData"></param>
        private DateTime ProcessNewLines(List<string> fileData, DateTime lastUpdate)
        {
            LogDataCollection logs = new LogDataCollection();
            UlsLineParser parser = new UlsLineParser();
            AbstractUlsLogEntry previousLog = null;
            bool foundHeader = false;

            // Loop through new/updated lines
            Logging.Writeline("LogFileWatcher.ProcessNewLines - {0} new lines found", fileData.Count);
            int insertCount = 0;
            foreach (string fileLine in fileData)
            {
                bool lineIsHeader = !foundHeader || fileLine.StartsWith("Timestamp");
                if (!lineIsHeader)
                {
                    // Try and get log-entry
                    AbstractUlsLogEntry thisLog = null;
                    bool lineIsFragment = false;
                    try
                    {
                        lineIsFragment = !UlsFileReader.GetLogEntryFromLine(fileLine, previousLog, out thisLog, parser);
                    }
                    catch (LineInvalidException)
                    {
                        // Add corrupt line
                        lineIsFragment = false;
                        thisLog = new FileLoadedUlsLogEntry();
                        thisLog.Message = "Cannot read ULS line '" + fileLine + "'";
                        thisLog.Level = "Read Fail";
                        thisLog.DateTime = DateTime.Now;
                        thisLog.Process = "ULS Studio";
                    }


                    // If line was valid and a whole line, add to collection
                    if (!lineIsFragment && thisLog is FileLoadedUlsLogEntry)
                    {
                        previousLog = thisLog;
                        logs.Add(thisLog);
                        insertCount++;
                    }
                }
                else
                    foundHeader = true;
            }

            Logging.Writeline("LogFileWatcher.ProcessNewLines - {0} new entries inserted.", insertCount);

            // Filter out logs which aren't older than the last update
            LogDataCollection removeIndexes = new LogDataCollection();
            foreach (FileLoadedUlsLogEntry newLogEntry in logs)
            {
                if (newLogEntry.DateTime <= lastUpdate)
                {
                    removeIndexes.Add(newLogEntry);
                }
            }

            // Remove anything that happened before the last log in the table
            foreach (AbstractUlsLogEntry log in removeIndexes)
            {
                logs.Remove(log);
            }

            // Return last update date from records?
            if (logs.Count > 0)
            {
                Logging.Writeline("LogFileWatcher - Found {0} new log-entries", logs.Count);

                // Raise event with new logs
                OnNewLogEntries(logs);

                DateTime lastRecordTimeStamp = logs.Last().DateTime;
                return lastRecordTimeStamp;
            }
            else
            {
                return lastUpdate;
            }
        }

        /// <summary>
        /// Reads last line of file + file contents
        /// </summary>
        private string GetLastFileContents(FileInfo fileToRead, List<string> fileContents)
        {
            using (FileStream fs = new FileStream(fileToRead.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                using (StreamReader file = new StreamReader(fs))
                {
                    try
                    {
                        string lastLine = string.Empty, line = string.Empty;
                        while ((line = file.ReadLine()) != null)
                        {
                            // Remember last line if it's not empty or null
                            if (!string.IsNullOrEmpty(line))
                            {
                                lastLine = line;
                                if (fileContents != null)
                                    fileContents.Add(line);
                            }

                        }
                        return lastLine;
                    }
                    finally
                    {
                        if (file != null) file.Close();
                    }

                }   // End of file

            }
            throw new OperationCanceledException();
        }

        /// <summary>
        /// Logfile data - fileInfo + last read line
        /// </summary>
        private class LogfileInfo
        {
            public LogfileInfo(FileInfo file, string lastReadLine)
            {
                if (lastReadLine == null)
                {
                    throw new ArgumentNullException("lastReadLine");
                }

                this.File = file;
                this.LastReadLine = lastReadLine;
            }
            public String LastReadLine { get; set; }
            public FileInfo File { get; set; }
        }
    }
}
